﻿using System;
using System.Collections.Generic;
using ch11.binary;
using ch11.binary.module;
using ch11.binary.type;
using ch11.instance;
using Math = ch11.binary.Math;

namespace ch11.interpreter
{
    public class Vm : IModule
    {
        internal OperandStack OperandStack;
        internal ControlStack ControlStack;
        internal Module Module;
        internal IMemory Memory;
        public IGlobal[] Globals;
        public uint Local0Idx;
        public VmFunc[] Funcs;
        public ITable Table;

        public Vm(Module mo)
        {
            OperandStack = new OperandStack {Slots = Array.Empty<ulong>()};
            ControlStack = new ControlStack(Array.Empty<ControlFrame>());
            Module = mo;
            Globals ??= Array.Empty<IGlobal>();
            Funcs ??= Array.Empty<VmFunc>();
        }

        public static (IModule, Exception err ) New(Module m, Dictionary<string, IModule> mm)
        {
            try
            {
                var inst = newVM(m, mm);
                return (inst, null);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }

        public static Vm newVM(Module m, Dictionary<string, IModule> mm)
        {
            var vm = new Vm(m);
            vm.LinkImports(mm);
            vm.InitFuncs();
            vm.InitTable();
            vm.InitMem();
            vm.InitGlobals();
            vm.ExecStartFunc();
            return vm;
        }

        private void ExecStartFunc()
        {
            if (Module.StartSec > 0)
            {
                var idx = Module.StartSec;
                Funcs[idx].call(null);
            }
        }
        /* linking */

        void LinkImports(Dictionary<string, IModule> mm)
        {
            foreach (var imp in Module.ImportSec)
            {
                var m = mm[imp.Module];
                if (m == null)
                {
                    Panic.Run(("module not found: " + imp.Module));
                }
                else
                {
                    linkImport(m, imp);
                }
            }
        }

        void linkImport(IModule m, Import imp)
        {
            var exported = m.GetMember(imp.Name);
            if (exported == null)
            {
                Panic.Run($"unknown import: {imp.Module}.{imp.Name}");
            }

            var typeMatched = false;
            // var x = exported.GetType();
            switch (exported)
            {
                case IFunction x:
                    if (imp.Desc.Tag == ConstData.ImportTagFunc)
                    {
                        var expectedFT = Module.TypeSec[imp.Desc.FuncType];
                        typeMatched = isFuncTypeMatch(expectedFT, x.Type());
                        Math.Append(ref Funcs, VmFunc.NewExternalFunc(expectedFT, x));
                    }

                    break;
                case ITable x:
                    if (imp.Desc.Tag == ConstData.ImportTagTable)
                    {
                        typeMatched = isLimitsMatch(imp.Desc.Table.Limits, x.Type().Limits);
                        Table = x;
                    }

                    break;
                case IMemory x:
                    if (imp.Desc.Tag == ConstData.ImportTagMem)
                    {
                        typeMatched = isLimitsMatch(imp.Desc.Mem, x.Type());
                        Memory = x;
                    }

                    break;
                case IGlobal x:
                    if (imp.Desc.Tag == ConstData.ImportTagGlobal)
                    {
                        typeMatched = isGlobalTypeMatch(imp.Desc.Global, x.Type());
                        Math.Append(ref Globals, x);
                    }

                    break;
            }

            if (!typeMatched)
            {
                Panic.Run($"incompatible import type: {imp.Module}.{imp.Name}");
            }
        }

        bool isGlobalTypeMatch(GlobalType expected, GlobalType actual)
        {
            return actual.ValType == expected.ValType &&
                   actual.Mut == expected.Mut;
        }

        /* helpers */
        bool isFuncTypeMatch(FuncType expected, FuncType actual)
        {
            return $"{expected}" == $"{actual}";
        }

        bool isLimitsMatch(Limits expected, Limits actual)
        {
            return actual.Min >= expected.Min &&
                   (expected.Max == 0 || actual.Max > 0 && actual.Max <= expected.Max);
        }

        internal void Loop()
        {
            var depth = ControlStack.ControlDepth();

            for (; ControlStack.ControlDepth() >= depth;)
            {
                var cf = ControlStack.TopControlFrame();
                if (cf.Pc == (cf.Instrs?.Length ?? 0))
                {
                    ExitBlock();
                }
                else
                {
                    if (cf.Instrs == null) continue;
                    var instr = cf.Instrs[cf.Pc];
                    cf.Pc++;
                    ExecInstr(instr);
                }
            }
        }

        public void InitMem()
        {
            if (Module.MemSec?.Length > 0)
            {
                Memory = new Memory(Module.MemSec[0]);
            }

            if (Module.DataSec == null)
            {
                return;
            }

            foreach (var data in Module.DataSec)
            {
                ExecConstExpr(data.Offset);

                Memory.Write(OperandStack.PopU64(), data.Init);
            }
        }

        public void InitGlobals()
        {
            if (Module.GlobalSec == null)
            {
                return;
            }

            foreach (var global in Module.GlobalSec)
            {
                ExecConstExpr(global.Init);
                Array.Resize(ref Globals, Globals.Length + 1);
                Globals[^1] = new GlobalVar(global.Type, PopU64());
            }
        }

        void ExecConstExpr(Instruction[] expr)
        {
            foreach (var instr in expr)
            {
                ExecInstr(instr);
            }
        }

        private void ExecInstr(Instruction instr)
        {
            //fmt.Printf("%s %v\n", instr.GetOpname(), instr.Args)
            var op = Instr.GetInstrFn()[instr.Opcode];
            op(this, instr.Args);
        }

        private int StackSize()
        {
            return OperandStack.StackSize();
        }

        /* block stack */
        public void EnterBlock(byte opcode, ref FuncType bt, ref Instruction[] instrs)
        {
            var bp = StackSize() - (bt.ParamTypes?.Length ?? 0);
            var cf = new ControlFrame(opcode, ref bt, ref instrs, bp);
            ControlStack.PushControlFrame(ref cf);
            if (opcode == OpCodes.Call)
            {
                Local0Idx = (uint) bp;
            }
        }

        public void InitFuncs()
        {
            // LinkNativeFuncs();
            for (var i = 0; i < Module.FuncSec.Length; i++)
            {
                var ftIdx = Module.FuncSec[i];
                var ft = Module.TypeSec[ftIdx];
                var code = Module.CodeSec[i];
                var newFuncs = new VmFunc[Funcs.Length + 1];
                Array.Copy(Funcs, newFuncs, Funcs.Length);
                newFuncs[^1] = new()
                {
                    Vm = this,
                    _type = ft,
                    Code = code,
                };
                Funcs = newFuncs;
            }
        }

        public static VmFunc NewInternalFunc(ref Vm vm, FuncType ft, Code code)
        {
            return new()
            {
                Vm = vm,
                _type = ft,
                Code = code,
            };
        }

        public void InitTable()
        {
            if (Module.TableSec.Length > 0)
            {
                Table = new Table(Module.TableSec[0]);
            }

            if (Module.ElemSec == null)
            {
                return;
            }

            foreach (var elem in Module.ElemSec)
            {
                ExecConstExpr(elem.Offset);

                var offset = PopU32();
                for (var i = 0; i < elem.Init.Length; i++)
                {
                    var funcIdx = elem.Init[i];
                    Table.SetElem(offset + (uint) i, Funcs[funcIdx]);
                }
            }
        }

        // private void LinkNativeFuncs()
        // {
        //     foreach (var imp in Module.ImportSec)
        //     {
        //         if (imp.Desc.Tag == ConstData.ImportTagFunc && imp.Module == "env")
        //         {
        //             var ft = Module.TypeSec[imp.Desc.FuncType];
        //             switch (imp.Name)
        //             {
        //                 case "print_char":
        //                     Funcs = Funcs.Append(VmFunc.NewExternalFunc(ft, Native.PrintChar)).ToArray();
        //                     break;
        //                 case "assert_true":
        //                     Funcs = Funcs.Append(VmFunc.NewExternalFunc(ft, Native.AssertTrue)).ToArray();
        //                     break;
        //                 case "assert_false":
        //                     Funcs = Funcs.Append(VmFunc.NewExternalFunc(ft, Native.AssertFalse)).ToArray();
        //                     break;
        //                 case "assert_eq_i32":
        //                     Funcs = Funcs.Append(VmFunc.NewExternalFunc(ft, Native.AssertEqI32)).ToArray();
        //                     break;
        //                 case "assert_eq_i64":
        //                     Funcs = Funcs.Append(VmFunc.NewExternalFunc(ft, Native.AssertEqI64)).ToArray();
        //                     break;
        //                 case "assert_eq_f32":
        //                     Funcs = Funcs.Append(VmFunc.NewExternalFunc(ft, Native.AssertEqF32)).ToArray();
        //                     break;
        //                 case "assert_eq_f64":
        //                     Funcs = Funcs.Append(VmFunc.NewExternalFunc(ft, Native.AssertEqF64)).ToArray();
        //                     break;
        //                 default:
        //                     Panic.Run("TODO");
        //                     break;
        //             }
        //         }
        //     }
        // }

        public void ExitBlock()
        {
            var cf = ControlStack.PopControlFrame();
            ClearBlock(ref cf);
        }

        private void ClearBlock(ref ControlFrame cf)
        {
            var results = PopU64S(cf.Bt.ResultTypes?.Length ?? 0);
            PopU64S(OperandStack.StackSize() - cf.Bp);
            PushU64S(results);
            if (cf.Opcode != OpCodes.Call || ControlStack.ControlDepth() <= 0) return;
            var (lastCallFrame, _) = ControlStack.TopCallFrame();
            if (lastCallFrame?.Bp != null) Local0Idx = (uint) lastCallFrame?.Bp;
        }

        public void ResetBlock(ControlFrame cf)
        {
            var results = PopU64S(cf.Bt.ParamTypes?.Length ?? 0);
            PopU64S(OperandStack.StackSize() - cf.Bp);
            PushU64S(results);
        }

        private ulong[] PopU64S(int n)
        {
            return OperandStack.PopU64S(n);
        }

        private void PushU64S(ulong[] vals)
        {
            OperandStack.PushU64S(vals);
        }

        public ulong GetOperand(uint idx)
        {
            return OperandStack.GetOperand(idx);
        }

        public uint PopU32()
        {
            return OperandStack.PopU32();
        }

        public ulong PopU64()
        {
            return OperandStack.PopU64();
        }

        public void PushU64(ulong val)
        {
            OperandStack.PushU64(val);
        }

        public void PushU32(uint val)
        {
            OperandStack.PushU32(val);
        }

        public void PushS32(int val)
        {
            OperandStack.PushS32(val);
        }

        public void PushS64(long val)
        {
            OperandStack.PushS64(val);
        }

        public bool PopBool()
        {
            return OperandStack.PopBool();
        }

        public ControlFrame PopControlFrame()
        {
            return ControlStack.PopControlFrame();
        }

        public object GetMember(string name)
        {
            foreach (var exp in Module.ExportSec)
            {
                if (exp.Name == name)
                {
                    var idx = exp.Desc.Idx;
                    switch (exp.Desc.Tag)
                    {
                        case ConstData.ExportTagFunc:
                            return Funcs[idx];
                        case ConstData.ExportTagTable:
                            return Table;
                        case ConstData.ExportTagMem:
                            return Memory;
                        case ConstData.ExportTagGlobal:
                            return Globals[idx];
                    }
                }
            }

            return null;
        }

        public (object[], Exception) InvokeFunc(string name, params object[] args)
        {
            var m = GetMember(name);
            if (m is IFunction f)
            {
                return f.Call(args);
            }

            return (null, new Exception("function not found: " + name));
        }

        public (object, Exception) GetGlobalVal(string name)
        {
            var m = GetMember(name);
            if (m is IGlobal g)
            {
                return (g.Get(), null);
            }

            return (null, new Exception("global not found: " + name));
        }

        public Exception SetGlobalVal(string name, object val)
        {
            var m = GetMember(name);
            if (m is IGlobal g)
            {
                g.Set(val);
                return null;
            }

            return new Exception("global not found: " + name);
        }
    }
}